/*
* Copyright (C) Microsoft. All rights reserved.  
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not 
* use this file except in compliance with the License.  You may obtain a copy 
* of the License at http://www.apache.org/licenses/LICENSE-2.0  
* 
* THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED 
* WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, 
* MERCHANTABLITY OR NON-INFRINGEMENT. 
*
* See the Apache License, Version 2.0 for specific language governing 
* permissions and limitations under the License.
*
*/
#include "pch.h"
#include <ppltasks.h>
#include <concurrent_queue.h>
#include <journal/models/interfaces.h>
#include <journal/models/page_model_factory.h>
#include <journal/models/db/notebook_table.h>
#include <journal/models/notebook_model.h>

using namespace journal::model;

notebook_model::notebook_model(const b::uint32& typeToken) :
    _pages(),
    _notebookReadyEvent(nullptr)
{
    // generic_model
    setGenericIdentity(0);
    setGenericTypeToken(typeToken);
    setGenericParent(nullptr);
    // tracked_object
    setTrackingService(constants::undoRedoService());
}

void notebook_model::initializeFromDB()
{
    std::vector<b::uint32> pageIds;
    _pageFactory->getPersistedPageIds(pageIds);
    std::vector<size_t> pageOrder, newPageOrder;
    _notebookTable->read(pageOrder);
    if (notebook_model::recoverPageOrder(pageIds, pageOrder, newPageOrder))
    {
        DEBUG_CONSOLE_TRACE("Recovered corrupt _pageOrder");
        _notebookTable->write(newPageOrder);
    }
}

std::shared_ptr<notebook_model> notebook_model::create(b::uint32 typeToken)
{
    std::shared_ptr<notebook_model> returnValue = std::make_shared<notebook_model>(typeToken);
    returnValue->_dbEngine = constants::storageService()->engine();
    returnValue->_notebookTable = journal::model::db::createNotebookTable(returnValue->_dbEngine);
    returnValue->_notebookTable->common()->initialize();

    returnValue->_pageFactory = journal::model::createPageModelFactory(returnValue->_dbEngine);

    returnValue->_notebookReadyEvent = ::CreateEventEx(nullptr, nullptr, CREATE_EVENT_MANUAL_RESET, EVENT_ALL_ACCESS);

    returnValue->initializeFromDB();

    returnValue->startLoadingAsync();

    return returnValue;
}

bool notebook_model::recoverPageOrder(std::vector<b::uint32> &pageIds, std::vector<size_t> &pageOrder, std::vector<size_t> &newPageOrder)
{
    if (pageIds.size() == 0)
    {
        return false;
    }

    // It's possible that the app crashed in such a way that _pageOrder refers 
    // to pages that do not exist in the page table. Remove them from _pageOrder
    // on entry.
    
    // I'm not sure if I can assume that pageIds is always {0, 1, 2 ...}
    // so I'm doing this the long way.

    b::uint32 maxPageId = 0;
    for (b::uint32 id : pageIds)
        if (id > maxPageId)
            maxPageId = id;

    std::vector<char> bitvect;
    bitvect.resize(maxPageId+1);
    for (b::uint32 i=0; i<=maxPageId; i++)
        bitvect[i] = 0;

    for (b::uint32 id : pageIds)
        bitvect[id] = 1;

    std::vector<size_t> fixedPageOrder;
    bool foundMissingPageId = false;
    for (size_t id : pageOrder)
    {
        if (id > maxPageId || bitvect[id] == 0)
            foundMissingPageId = true;
        else
            fixedPageOrder.push_back(id);
    }

    pageOrder.clear();
    pageOrder = fixedPageOrder;

    // We can also get into the case where _pageOrder has dupilcate entries.
    // I'm not quite sure how this can happen yet, so I'm asserting, but
    // handling the case in retail builds.
        
    for (b::uint32 i=0; i<=maxPageId; i++)
        bitvect[i] = 0;

    bool foundDuplicatePageId = false;
    for (size_t id : pageOrder)
    {
        if (bitvect[id] == 1)
        {
            BAJA_VERIFY(false);
            foundDuplicatePageId = true;
        }
        else
        {
            newPageOrder.push_back(id);
            bitvect[id] = 1;
        }
    }

    return foundMissingPageId || foundDuplicatePageId;
}

b::uint32 notebook_model::pageCount()
{
    WaitForSingleObjectEx(_notebookReadyEvent, INFINITE, FALSE);

    BAJA_LOCK(_mutex);

    return static_cast<b::uint32>(_pageOrder.size());
}

b::int32 notebook_model::addObserver(const std::shared_ptr<inotebook_model_observer>& observer)
{
    DEBUG_CONSOLE_TRACE_TAG(TRACE_TAG_MODEL,
        boost::wformat(L">> <<>> notebook_model::addObserver() : an observer has been added.")
        );

    return _observerSource += observer;
}

void notebook_model::removeObserver(const std::shared_ptr<inotebook_model_observer>& observer)
{
    _observerSource -= observer;
}

void notebook_model::removeObserver(const b::int32 cookie)
{
    _observerSource -= cookie;
}

void notebook_model::onPageInitialized(const std::shared_ptr<ipage_model>& page)
{
    DWORD retCode = WaitForSingleObjectEx(_notebookReadyEvent, 0, FALSE);

    if (retCode == WAIT_TIMEOUT)
    {
        // We are not ready to fire events.
        return;
    }

    DEBUG_CONSOLE_TRACE_TAG(TRACE_TAG_MODEL,
        boost::wformat(L">> <<>> notebook_model::onPageInitialized() : firing onPageModelCreated(page = %i)") %
        page->storageId()
        );

    _observerSource.raise(
        [&](const std::shared_ptr<inotebook_model_observer>& observer, void* context)
        {
            observer->onPageModelCreated(page);
        });
}

b::uint32 notebook_model::storageId()
{
    return genericIdentity();
}

void notebook_model::onPageLoadLevelSet(const std::shared_ptr<ipage_model>& page, const page_level currentLevel)
{
    DWORD retCode = WaitForSingleObjectEx(_notebookReadyEvent, 0, FALSE);

    if (retCode == WAIT_TIMEOUT)
    {
        // We are not ready to fire events.
        return;
    }

    DEBUG_CONSOLE_TRACE_TAG(TRACE_TAG_MODEL,
        boost::wformat(L">> <<>> notebook_model::onPageLoadLevelSet() : firing onPageLoadLevelSet(page = %i, level = %i)") %
        page->storageId() %
        static_cast<b::uint32>(currentLevel)
        );

    _observerSource.raise(
        [&](const std::shared_ptr<inotebook_model_observer>& observer, void* context)
        {
            observer->onPageLoadLevelSet(page, currentLevel);
        });
}

void notebook_model::onChildChanged(
    const b::uint32& typeToken,
    const b::uint32& genericIdentity
    )
{
}

void notebook_model::onPhotoModelAdded(
    const std::shared_ptr<iphoto_model>& photo)
{
}

void notebook_model::onPhotoModelRemoved(
    const std::shared_ptr<iphoto_model>& photo)
{
}

void notebook_model::onPhotoModelBroughtToFront(
    const std::shared_ptr<iphoto_model>& photo)
{
}

void notebook_model::onInkSurfaceModelReady(
    const std::shared_ptr<iink_surface_model>& inkSurface)
{
}

void notebook_model::onThumbnailSaved(
    const std::shared_ptr<ipage_model>& page)
{
}

void notebook_model::onPageModelPaperTypeChanged(
    const b::uint32& paperType)
{
}

void notebook_model::loadChildrenLinks()
{
    DEBUG_CONSOLE_TRACE_TAG(TRACE_TAG_MODEL,
        boost::wformat(L">> notebook_model::loadChildrenLinks()")
        );

    _notebookTable->read(_pageOrder);
    this->recomputePageIdMap();
}

// Make map from _pageOrder
void notebook_model::recomputePageIdMap()
{
    _pageIdToPageNumberMap.clear();

    size_t pageNumber = 0;
    for (size_t pageId : _pageOrder)
    {
        _pageIdToPageNumberMap[static_cast<b::uint32>(pageId)] = pageNumber;
        pageNumber++;
    }
}

void notebook_model::movePage(size_t fromNumber, size_t toNumber)
{
    BAJA_LOCK(_mutex);

    size_t fromPageOrder = _pageOrder[fromNumber];

    if (fromNumber < toNumber)
    {
        for (size_t i=fromNumber; i<toNumber; i++)
        {
            _pageOrder[i] = _pageOrder[i + 1];
        }
    }
    else
    {
        for (size_t i=fromNumber; i>toNumber; i--)
        {
            _pageOrder[i] = _pageOrder[i - 1];
        }
    }

    _pageOrder[toNumber] = fromPageOrder;

    _notebookTable->write(_pageOrder);
    this->recomputePageIdMap();
}

void notebook_model::removePage(b::uint32 pageId)
{
    std::shared_ptr<ipage_model> page = _pages[pageId];
    unobservePage(page);

    detachPage(page);
}

void notebook_model::loadPageAsync(
    const b::uint32& pageId,
    const page_level& targetLevel)
{
    WaitForSingleObjectEx(_notebookReadyEvent, INFINITE, FALSE);

    BAJA_LOCK(_mutex);

    DEBUG_CONSOLE_TRACE_TAG(TRACE_TAG_MODEL,
        boost::wformat(L">> notebook_model::loadPageAsync(pageId = %i,level = %i)") %
        pageId %
        static_cast<b::uint32>(targetLevel)
        );

    if ((pageId < _pages.size()) && (_pages[pageId] != nullptr))
    {
        std::shared_ptr<ipage_model> targetPage = _pages[pageId];
        if (targetLevel == page_level::page_deleted)
        {
            this->removePage(pageId);
        }
        _pageCacheManager.push(targetPage, targetLevel);
    }
}

void notebook_model::emptyLoadQueue()
{
    _pageCacheManager.clear();
}

void notebook_model::ensureOnePage()
{
    if (_pageOrder.size() == 0)
    {
        createPageObject();
    }
}

void notebook_model::startLoadingAsync()
{
    DEBUG_CONSOLE_TRACE_TAG(TRACE_TAG_MODEL,
        boost::wformat(L"-- notebook_model::startLoadingAsync()")
        );

    std::shared_ptr<notebook_model> p_this = std::static_pointer_cast<notebook_model>(shared_from_this());
    concurrency::task<void> t([p_this]()
    {
        p_this->loadChildrenLinks();
        p_this->ensureOnePage();
        SetEvent(p_this->_notebookReadyEvent);
        p_this->_pageCacheManager.run();
    });
}

void notebook_model::cancelLoadingAsync()
{
    _pageCacheManager.stopAsync();
}

#ifdef DEBUG_ENABLE_MODELS
void notebook_model::dump(bool includeChildren, std::wstring prefix)
{
    DEBUG_CONSOLE_TRACE_TAG(TRACE_TAG_MODEL,
        boost::wformat(L"%s : notebook_model") %
        prefix.c_str()
        );

    if (includeChildren == false)
    {
        return;
    }

    concurrency::concurrent_vector<std::shared_ptr<ipage_model>>::iterator pageIt;
    for (pageIt = _pages.begin(); pageIt < _pages.end(); ++pageIt)
    {
        DEBUG_MODEL_DUMP((*pageIt), includeChildren, prefix);
    }
}
#endif

void notebook_model::observePage(const std::shared_ptr<ipage_model>& page)
{
    if (page == nullptr)
    {
        return;
    }
    observed_page observedPage;

    observedPage.pageCookie = page->addObserver(std::dynamic_pointer_cast<ipage_model_observer>(shared_from_this()));

    std::shared_ptr<iinternal_page_model> internalPage = std::dynamic_pointer_cast<iinternal_page_model>(page);
    observedPage.internalPageCookie = internalPage->addInternalObserver(std::dynamic_pointer_cast<iinternal_page_model_observer>(shared_from_this()));

    std::shared_ptr<bm::igeneric_model> genericModel = std::dynamic_pointer_cast<bm::igeneric_model>(page);
    observedPage.genericPageCookie = genericModel->addGenericObserver(std::dynamic_pointer_cast<bm::igeneric_model_observer>(shared_from_this()));

    _observedObjects.pages[page->storageId()] = observedPage;
}

void notebook_model::unobservePage(const std::shared_ptr<ipage_model>& page)
{
    if (page == nullptr)
    {
        return;
    }
    observed_page observedPage = _observedObjects.pages[page->storageId()];

    if (observedPage.pageCookie != -1)
    {
        page->removeObserver(observedPage.pageCookie);
        observedPage.pageCookie = -1;
    }

    if (observedPage.internalPageCookie != -1)
    {
        std::shared_ptr<iinternal_page_model> internalPage = std::dynamic_pointer_cast<iinternal_page_model>(page);
        internalPage->removeInternalObserver(observedPage.internalPageCookie);
        observedPage.internalPageCookie = -1;
    }

    if (observedPage.genericPageCookie != -1)
    {
        std::shared_ptr<bm::igeneric_model> genericModel = std::dynamic_pointer_cast<bm::igeneric_model>(page);
        genericModel->removeGenericObserver(observedPage.genericPageCookie);
        observedPage.genericPageCookie = -1;
    }

    _observedObjects.pages.erase(page->storageId());
}

void notebook_model::attachPage(
    const std::shared_ptr<ipage_model>& page)
{
    BAJA_LOCK(_mutex);

    if (page->storageId() >= _pages.size())
    {
        _pages.resize(page->storageId() + 1);
    }
    _pages[page->storageId()] = page;

    b::uint32 pageNumber = static_cast<b::uint32>(_pageOrder.size());
    _pageOrder.push_back(page->storageId());
    _pageIdToPageNumberMap[page->storageId()] = pageNumber; // instead of recomputing the whole thing
}

void notebook_model::reattachPage(
    const std::shared_ptr<ipage_model>& page)
{
     _pages[page->storageId()] = page;
}

void notebook_model::detachPage(
    const std::shared_ptr<ipage_model>& page)
{
    if (page == nullptr)
    {
        return;
    }
    size_t pageId = page->storageId();

    _pages[pageId] = nullptr;
    std::vector<size_t> pageOrder;
    pageOrder.reserve(_pageOrder.size() - 1);
    for (size_t id : _pageOrder)
    {
        if (id != pageId)
        {
            pageOrder.push_back(id);
        }
    }
    _pageOrder.clear();
    _pageOrder = pageOrder;
    _notebookTable->write(_pageOrder);
    this->recomputePageIdMap();
}

std::shared_ptr<ipage_model> notebook_model::recreatePageObject(
    b::uint32 pageId)
{
    std::shared_ptr<ipage_model> page = _pageFactory->recreatePage(shared_from_this(), pageId);

    observePage(page);

    reattachPage(page);

    return page;
}

b::uint32 notebook_model::createPageObject()
{
    std::shared_ptr<ipage_model> page = _pageFactory->createPage(shared_from_this());

    observePage(page);

    attachPage(page);

    // only after attaching, we can initialize...
    std::shared_ptr<iinternal_page_model> internalPage = std::dynamic_pointer_cast<iinternal_page_model>(page);
    internalPage->initialize(static_cast<b::uint32>(constants::general::paperType()));

    // write to disk
    concurrency::task<void> t(
        [this, internalPage]()
        {
            BAJA_LOCK(_mutex);

            internalPage->createInDB();
            _notebookTable->write(_pageOrder);
        });

    return page->storageId();
}

b::uint32 notebook_model::createPageModel()
{
    WaitForSingleObjectEx(_notebookReadyEvent, INFINITE, FALSE);

    return createPageObject();
}

std::shared_ptr<ipage_model> notebook_model::getPage(const b::uint32 pageNumber)
{
    WaitForSingleObjectEx(_notebookReadyEvent, INFINITE, FALSE);

    BAJA_LOCK(_mutex);

    b::uint32 pageId = static_cast<b::uint32>(_pageOrder[pageNumber]);

    // Does page list has a spot for pageNumber?
    if (pageId >= _pages.size())
    {
        _pages.resize(pageId + 1);
    }

    std::shared_ptr<ipage_model> page;

    // Does the spot point to an object in memory?
    if (_pages[pageId] == nullptr)
    {
        page = recreatePageObject(pageId);
    }

    return _pages[pageId];
}

size_t notebook_model::getPageNumberFromPageId(size_t pageId)
{
    BAJA_LOCK(_mutex);

    size_t pageNumber = _pageIdToPageNumberMap[pageId];

#ifdef BAJA_DEBUG
    // appropriate paranoia
    bool foundMatch = true;
    b::uint32 index = 0;
    for (size_t id : _pageOrder)
    {
        if (id == pageId)
        {
            BAJA_VERIFY(pageNumber == index);
            foundMatch = true;
            break;
        }
        index++;
    }
    BAJA_VERIFY(foundMatch);
#endif

    return pageNumber;
}

size_t notebook_model::getPageIdFromPageNumber(const size_t pageNumber)
{
    BAJA_LOCK(_mutex);

    return _pageOrder[pageNumber];
}